package org.mspring.platform.dao.support;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;

import org.apache.commons.lang.StringUtils;
import org.mspring.platform.dao.BaseDao;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class AbstractDao<T> implements BaseDao<T> {
    private static final Logger logger = LoggerFactory.getLogger(AbstractDao.class);
    private Class<T> entityClass;
    private EntityManager entityManager;

    protected AbstractDao(Class<T> entityClass) {
        this.entityClass = entityClass;
    }

    @PersistenceContext
    public void setEntityManager(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    public <PK extends Serializable> T get(PK id) {
        return this.entityManager.getReference(this.entityClass, id);
    }

    public void save(T entity) {
        this.entityManager.persist(entity);
    }

    public void update(T entity) {
        this.entityManager.merge(entity);
    }

    public void delete(T entity) {
        this.entityManager.remove(entity);
    }

    public <PK extends Serializable> void delete(PK id) {
        Object entity = get(id);
        this.entityManager.remove(entity);
    }

    public T findUnique(QueryAppender appender) {
        TuplePair tuplePair = buildQueryString(appender);
        String queryString = (String) tuplePair.getTuple1();
        Object[] values = (Object[]) tuplePair.getTuple2();
        return findUnique(queryString, values);
    }

    public T findUnique(String queryString, Object value) {
        return findUnique(queryString, new Object[] { value });
    }

    public T findUnique(String queryString, Object[] values) {
        TypedQuery query = this.entityManager.createQuery(queryString, this.entityClass);
        setParametersToQuery(query, values);
        T t = null;
        try {
            Object object = query.getSingleResult();
            if (object != null)
                t = (T) object;
        } catch (NoResultException e) {
            logger.warn(e.getMessage());
        }
        return t;
    }

    public <X> X findUnique(String queryString, Class<X> retType, Object[] values) {
        TypedQuery query = this.entityManager.createQuery(queryString, retType);
        setParametersToQuery(query, values);
        X x = null;
        try {
            Object object = query.getSingleResult();
            if (object != null)
                x = (X) object;
        } catch (NoResultException e) {
            logger.warn(e.getMessage());
        }
        return x;
    }

    public Page<T> findPage(Page<T> page, QueryAppender appender) {
        TuplePair tuplePair = buildQueryString(appender);
        String queryString = (String) tuplePair.getTuple1();
        Object[] values = (Object[]) tuplePair.getTuple2();
        return findPage(page, queryString, values);
    }

    public Page<T> findPage(Page<T> page, String queryString, Object[] values) {
        if (page.isAutoCount()) {
            Long totalCount = count(queryString, values);
            page.setTotalCount(totalCount.longValue());
            page.setAutoCount(false);
        }

        TypedQuery query = this.entityManager.createQuery(queryString, this.entityClass);

        setParametersToQuery(query, values);
        query.setFirstResult(page.getFirst() - 1);
        query.setMaxResults(page.getPageSize());
        return page.setResult(query.getResultList());
    }

    public List<T> find(QueryAppender appender) {
        TuplePair tuplePair = buildQueryString(appender);
        String queryString = (String) tuplePair.getTuple1();
        Object[] values = (Object[]) tuplePair.getTuple2();
        return find(queryString, values);
    }

    public List<T> find(String queryString, Object value) {
        return find(queryString, new Object[] { value });
    }

    public List<T> find(String queryString) {
        TypedQuery<T> query = this.entityManager.createQuery(queryString, this.entityClass);
        return query.getResultList();
    }

    public List<T> find(String queryString, Object[] values) {
        TypedQuery<T> query = this.entityManager.createQuery(queryString, this.entityClass);
        setParametersToQuery(query, values);
        return query.getResultList();
    }

    public <X> List<X> find(String queryString, Class<X> retType, Object[] values) {
        TypedQuery<X> query = this.entityManager.createQuery(queryString, retType);
        setParametersToQuery(query, values);
        return query.getResultList();
    }

    public long count(QueryAppender appender) {
        TuplePair tuplePair = buildQueryString(appender);
        String queryString = (String) tuplePair.getTuple1();
        Object[] values = (Object[]) tuplePair.getTuple2();
        return count(queryString, values).longValue();
    }

    public Long count(String queryString, Object[] values) {
        TypedQuery countQuery = getCountQuery(queryString, values);
        return (Long) countQuery.getSingleResult();
    }

    public List<T> findAll() {
        String entityName = this.entityClass.getSimpleName();
        String entityAlias = StringUtils.uncapitalize(entityName);
        String queryString = "from " + entityName + " as " + entityAlias;
        return this.find(queryString);
    }

    protected void setParametersToQuery(Query query, Object[] values) {
        for (int i = 1; i <= values.length; i++)
            query.setParameter(i, values[(i - 1)]);
    }

    protected EntityManager getEntityManager() {
        return this.entityManager;
    }

    protected CriteriaBuilder getCriteriaBuilder() {
        return this.entityManager.getCriteriaBuilder();
    }

    private TypedQuery<Long> getCountQuery(String queryString, Object[] values) {
        String countQueryStr = queryString;

        countQueryStr = "FROM " + StringUtils.substringAfter(countQueryStr, "FROM");
        countQueryStr = StringUtils.substringBefore(countQueryStr, "ORDER BY");
        countQueryStr = StringUtils.substringBefore(countQueryStr, "GROUP BY");
        countQueryStr = "SELECT COUNT(*) " + countQueryStr;
        TypedQuery query = this.entityManager.createQuery(countQueryStr, Long.class);
        setParametersToQuery(query, values);
        return query;
    }

    private TuplePair<String, Object[]> buildQueryString(QueryAppender appender) {
        String entityName = this.entityClass.getSimpleName();
        String entityAlias = StringUtils.uncapitalize(entityName);
        StringBuffer buffer = new StringBuffer();
        buffer.append(" FROM ").append(entityName).append(" AS ").append(entityAlias);
        List values = new ArrayList();
        int current;
        int position;
        if (!appender.isConditionsEmpty()) {
            buffer.append(" WHERE");
            current = 1;
            position = 1;
            for (Map.Entry condition : appender.getConditions().entrySet()) {
                if (current > 1) {
                    buffer.append(" AND");
                }
                if (((String) condition.getKey()).contains("IN")) {
                    buffer.append(" ").append((String) condition.getKey()).append(" (").append("?").append(position++).append(")");

                    values.add(condition.getValue());
                } else if (((String) condition.getKey()).contains("BETWEEN")) {
                    buffer.append(" ").append((String) condition.getKey()).append(" ").append("?").append(position++).append(" AND ").append("?");

                    Object[] valuePair = (Object[]) (Object[]) condition.getValue();
                    values.add(valuePair[0]);
                    values.add(valuePair[1]);
                } else {
                    buffer.append(" ").append((String) condition.getKey()).append("?").append(position++);

                    values.add(condition.getValue());
                }
                current++;
            }
        }
        if (!appender.isOrdersEmpty()) {
            buffer.append(" ORDER BY");
            current = 1;
            for (Map.Entry order : appender.getOrders().entrySet()) {
                if (current > 1) {
                    buffer.append(",");
                }
                buffer.append(" ").append((String) order.getKey()).append(" ").append((String) order.getValue());

                current++;
            }
        }
        return new TuplePair(buffer.toString(), values.toArray());
    }
}